/* ===========================================================
 * TradeManager : a application to trade strategies for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2011-2011, by Simon Allen and Contributors.
 *
 * Project Info:  org.trade
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * [Java is a trademark or registered trademark of Oracle, Inc.
 * in the United States and other countries.]
 *
 * (C) Copyright 2011-2011, by Simon Allen and Contributors.
 *
 * Original Author:  Simon Allen;
 * Contributor(s):   -;
 *
 * Changes
 * -------
 *
 */
package org.trade.ui.base;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterAbortException;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.text.MessageFormat;

import javax.print.DocFlavor;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.swing.JComponent;
import javax.swing.JTable.PrintMode;
import javax.swing.SwingUtilities;

/**
 * 
 * @version $Id: JComponentVista.java,v 1.2 2001/10/22 18:55:37 simon Exp $
 * @author Simon Allen
 */
public class ComponentPrintService extends PrintPage implements Printable {

	private double m_ScaleX;

	private double m_ScaleY;

	/**
	 * The Swing component to print.
	 */
	private Component m_component = null;

	private Throwable printError;

	/**
	 * Create a Pageable that can print a Swing JComponent over multiple pages.
	 * 
	 * @param c
	 *            The swing JComponent to be printed.
	 * 
	 * @param format
	 *            The size of the pages over which the componenent will be
	 *            printed.
	 */
	public ComponentPrintService(Component c, PageFormat format) {
		setPageFormat(format);
		setPrintable(this);
		setComponent(c);

		/*
		 * Tell the Vista we subclassed the size of the canvas.
		 */

		Rectangle componentBounds = c.getBounds(null);

		setSize(componentBounds.width, componentBounds.height);
		setScale(1, 1);
	}

	/**
	 * Method setComponent.
	 * 
	 * @param c
	 *            Component
	 */
	protected void setComponent(Component c) {
		m_component = c;
	}

	/**
	 * Method setScale.
	 * 
	 * @param scaleX
	 *            double
	 * @param scaleY
	 *            double
	 */
	protected void setScale(double scaleX, double scaleY) {
		m_ScaleX = scaleX;
		m_ScaleY = scaleY;
	}

	public void scaleToFitX() {
		PageFormat format = getPageFormat();
		Rectangle componentBounds = m_component.getBounds(null);
		double scaleX = format.getImageableWidth() / componentBounds.width;
		double scaleY = scaleX;

		if (scaleX < 1) {
			setSize((float) format.getImageableWidth(),
					(float) (componentBounds.height * scaleY));
			setScale(scaleX, scaleY);
		}
	}

	public void scaleToFitY() {
		PageFormat format = getPageFormat();
		Rectangle componentBounds = m_component.getBounds(null);
		double scaleY = format.getImageableHeight() / componentBounds.height;
		double scaleX = scaleY;

		if (scaleY < 1) {
			setSize((float) (componentBounds.width * scaleX),
					(float) format.getImageableHeight());
			setScale(scaleX, scaleY);
		}
	}

	/**
	 * Method scaleToFit.
	 * 
	 * @param useSymmetricScaling
	 *            boolean
	 */
	public void scaleToFit(boolean useSymmetricScaling) {
		PageFormat format = getPageFormat();
		Rectangle componentBounds = m_component.getBounds(null);
		double scaleX = format.getImageableWidth() / componentBounds.width;
		double scaleY = format.getImageableHeight() / componentBounds.height;

		if ((scaleX < 1) || (scaleY < 1)) {
			if (useSymmetricScaling) {
				if (scaleX < scaleY) {
					scaleY = scaleX;
				} else {
					scaleX = scaleY;
				}
			}

			setSize((float) (componentBounds.width * scaleX),
					(float) (componentBounds.height * scaleY));
			setScale(scaleX, scaleY);
		}
	}

	/**
	 * Method print.
	 * 
	 * @param graphics
	 *            Graphics
	 * @param pageFormat
	 *            PageFormat
	 * @param pageIndex
	 *            int
	 * @return int
	 * @throws PrinterException
	 * @see java.awt.print.Printable#print(Graphics, PageFormat, int)
	 */
	public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
			throws PrinterException {

		if (pageIndex > 0) {
			return NO_SUCH_PAGE;
		}
		Graphics2D g2 = (Graphics2D) graphics;

		g2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());

		Rectangle componentBounds = m_component.getBounds(null);

		g2.translate(-componentBounds.x, -componentBounds.y);
		g2.scale(m_ScaleX, m_ScaleY);

		boolean wasBuffered = disableDoubleBuffering(m_component);
		m_component.printAll(g2);
		restoreDoubleBuffering(m_component, wasBuffered);
		return PAGE_EXISTS;
	}

	/**
	 * Method print.
	 * 
	 * @return boolean
	 * @throws PrinterException
	 * @throws HeadlessException
	 */
	public boolean print() throws PrinterException, HeadlessException {
		boolean showDialogs = !GraphicsEnvironment.isHeadless();

		return print(PrintMode.FIT_WIDTH, null, null, showDialogs, null,
				showDialogs, null);
	}

	/**
	 * Method print.
	 * 
	 * @param printMode
	 *            PrintMode
	 * @param headerFormat
	 *            MessageFormat
	 * @param footerFormat
	 *            MessageFormat
	 * @param showPrintDialog
	 *            boolean
	 * @param attr
	 *            PrintRequestAttributeSet
	 * @param interactive
	 *            boolean
	 * @param service
	 *            PrintService
	 * @return boolean
	 * @throws PrinterException
	 * @throws HeadlessException
	 */
	public boolean print(PrintMode printMode, MessageFormat headerFormat,
			MessageFormat footerFormat, boolean showPrintDialog,
			PrintRequestAttributeSet attr, boolean interactive,
			PrintService service) throws PrinterException, HeadlessException {
		// complain early if an invalid parameter is specified for headless mode
		boolean isHeadless = GraphicsEnvironment.isHeadless();
		if (isHeadless) {
			if (showPrintDialog) {
				throw new HeadlessException("Can't show print dialog.");
			}

			if (interactive) {
				throw new HeadlessException("Can't run interactively.");
			}
		}

		// Get a PrinterJob.
		// Do this before anything with side-effects since it may throw a
		// security exception - in which case we don't want to do anything else.
		final PrinterJob job = PrinterJob.getPrinterJob();

		if (attr == null) {
			attr = new HashPrintRequestAttributeSet();
		}

		// fetch the Printable
		Printable printable = this;

		DocFlavor doc_flavor = DocFlavor.INPUT_STREAM.JPEG;
		PrintRequestAttributeSet attr_set = new HashPrintRequestAttributeSet();
		PrintService[] services = PrintServiceLookup.lookupPrintServices(
				doc_flavor, attr_set);
		if (services.length > 0) {
			service = services[1];
		}

		if (interactive) {
			// wrap the Printable so that we can print on another thread
			printable = new ThreadSafePrintable(printable);
		}

		// set the printable on the PrinterJob
		job.setPrintable(printable);

		// if specified, set the PrintService on the PrinterJob
		if (service != null) {
			job.setPrintService(service);
		}

		// if requested, show the print dialog
		if (showPrintDialog && !job.printDialog(attr)) {
			// the user cancelled the print dialog
			return false;
		}

		// if not interactive, just print on this thread (no dialog)
		if (!interactive) {
			// do the printing
			job.print(attr);

			// we're done
			return true;
		}

		// make sure this is clear since we'll check it after
		printError = null;

		// to synchronize on
		final Object lock = new Object();

		// copied so we can access from the inner class
		final PrintRequestAttributeSet copyAttr = attr;

		// this runnable will be used to do the printing
		// (and save any throwables) on another thread
		Runnable runnable = new Runnable() {
			public void run() {
				try {
					// do the printing
					job.print(copyAttr);
				} catch (Throwable t) {
					// save any Throwable to be rethrown
					synchronized (lock) {
						printError = t;
					}
				}
			}
		};

		// start printing on another thread
		Thread th = new Thread(runnable, "PrinterThread");
		th.start();

		// look for any error that the printing may have generated
		Throwable pe;
		synchronized (lock) {
			pe = printError;
			printError = null;
		}

		// check the type of error and handle it
		if (pe != null) {
			// a subclass of PrinterException meaning the job was aborted,
			// in this case, by the user
			if (pe instanceof PrinterAbortException) {
				return false;
			} else if (pe instanceof PrinterException) {
				throw (PrinterException) pe;
			} else if (pe instanceof RuntimeException) {
				throw (RuntimeException) pe;
			} else if (pe instanceof Error) {
				throw (Error) pe;
			}

			// can not happen
			throw new AssertionError(pe);
		}

		return true;

	}

	/**
	 * Method disableDoubleBuffering.
	 * 
	 * @param c
	 *            Component
	 * @return boolean
	 */
	private boolean disableDoubleBuffering(Component c) {
		if (!(c instanceof JComponent))
			return false;
		JComponent jc = (JComponent) c;
		boolean wasBuffered = jc.isDoubleBuffered();
		jc.setDoubleBuffered(false);
		return wasBuffered;
	}

	/**
	 * Method restoreDoubleBuffering.
	 * 
	 * @param c
	 *            Component
	 * @param wasBuffered
	 *            boolean
	 */
	private void restoreDoubleBuffering(Component c, boolean wasBuffered) {
		if (c instanceof JComponent)
			((JComponent) c).setDoubleBuffered(wasBuffered);
	}

	/**
	 */
	private class ThreadSafePrintable implements Printable {

		/** The delegate <code>Printable</code>. */
		private Printable printDelegate;

		/**
		 * To communicate any return value when delegating.
		 */
		private int retVal;

		/**
		 * To communicate any <code>Throwable</code> when delegating.
		 */
		private Throwable retThrowable;

		/**
		 * Construct a <code>ThreadSafePrintable</code> around the given
		 * delegate.
		 * 
		 * @param printDelegate
		 *            the <code>Printable</code> to delegate to
		 */
		public ThreadSafePrintable(Printable printDelegate) {
			this.printDelegate = printDelegate;
		}

		/**
		 * Prints the specified page into the given {@link Graphics} context, in
		 * the specified format.
		 * <p>
		 * Regardless of what thread this method is called on, all calls into
		 * the delegate will be done on the event-dispatch thread.
		 * 
		 * @param graphics
		 *            the context into which the page is drawn
		 * @param pageFormat
		 *            the size and orientation of the page being drawn
		 * @param pageIndex
		 *            the zero based index of the page to be drawn
		 * 
		 * 
		 * @return PAGE_EXISTS if the page is rendered successfully, or
		 *         NO_SUCH_PAGE if a non-existent page index is specified * @throws
		 *         PrinterException if an error causes printing to be aborted * @see
		 *         java.awt.print.Printable#print(Graphics, PageFormat, int)
		 */
		public int print(final Graphics graphics, final PageFormat pageFormat,
				final int pageIndex) throws PrinterException {

			// We'll use this Runnable
			Runnable runnable = new Runnable() {
				public synchronized void run() {
					try {
						// call into the delegate and save the return value
						retVal = printDelegate.print(graphics, pageFormat,
								pageIndex);
					} catch (Throwable throwable) {
						// save any Throwable to be rethrown
						retThrowable = throwable;
					} finally {
						// notify the caller that we're done
						notifyAll();
					}
				}
			};

			synchronized (runnable) {
				// make sure these are initialized
				retVal = -1;
				retThrowable = null;

				// call into the EDT
				SwingUtilities.invokeLater(runnable);

				// wait for the runnable to finish
				while (retVal == -1 && retThrowable == null) {
					try {
						runnable.wait();
					} catch (InterruptedException ie) {
						// short process, safe to ignore interrupts
					}
				}

				// if the delegate threw a throwable, rethrow it here
				if (retThrowable != null) {
					if (retThrowable instanceof PrinterException) {
						throw (PrinterException) retThrowable;
					} else if (retThrowable instanceof RuntimeException) {
						throw (RuntimeException) retThrowable;
					} else if (retThrowable instanceof Error) {
						throw (Error) retThrowable;
					}

					// can not happen
					throw new AssertionError(retThrowable);
				}

				return retVal;
			}
		}
	}
}
